Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid circular dependencies & add dependency-cruiser #475

Merged
merged 1 commit into from
Nov 13, 2018
Merged

Conversation

BPScott
Copy link
Member

@BPScott BPScott commented Oct 26, 2018

WHY are these changes introduced?

In another issue @GoodForOneFare pointed out that having circular dependencies causes pain downstream.

If we were to remove circular dependencies then sewing-kit should be able optimize polaris-react more effectively.

This basically means changing how we import components - we should reference individual components specifically instead of importing from the components index. For example, assuming the following code exists in src/components/MyComponent/MyComponent.tsx:

Bad, as it results in circular dependencies:

import {Avatar, Card, Stack, TextStyle} from '..';

Good, no circular dependencies:

import Avatar from '../Avatar';
import Card from '../Card';
import Stack from '../Stack';
import TextStyle from '../TextStyle';

WHAT is this pull request doing?

Updates most existing cases where we import from the components index to import from specific components instead.

Adds https://github.com/sverweij/dependency-cruiser as a tool to audit these dependencies.

Run yarn run depcruise src --validate to see a report.

I have not yet added the above to run as part of CI as there is still some complexity that needs tiding that isn't as simple as renaming imports.

How to 🎩

  • Run yarn run depcruise src --validate to check existing errors
  • Check tests / linting to assert things still compile and run as expected
  • Add an import from the components index, and see that running depcruise alerts on this problem

@BPScott BPScott requested a review from kvendrik October 26, 2018 01:04
@BPScott BPScott temporarily deployed to polaris-react-pr-475 October 26, 2018 01:04 Inactive
Copy link
Member

@kvendrik kvendrik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm all for optimizing our bundle and this seems like an awesome tool. I still have questions about the dont-import-from-index thing though. Not doing that creates quite a bit of repetitiveness and I want to make sure we're going down the right path.

What is the reason we can't import from index but Web and other sewing-kit apps are recommended to do so?

Also if we are going to switch over to specific imports only I think we should probably create a custom linting rule that explains why especially since this is the preferred way of importing in some of our other codebases.

@BPScott BPScott changed the title Add Dependency Cruiser Avoid circular dependencies & add dependency-cruiser Oct 26, 2018
@BPScott
Copy link
Member Author

BPScott commented Oct 26, 2018

Also if we are going to switch over to specific imports only I think we should probably create a custom linting rule that explains why especially since this is the preferred way of importing in some of our other codebases.

My original plan was to add depcruiser as part of the linting phase, as while we use eslint-plugin-import (via eslint-plugin-shopify) and it has a no cylical imports rule it was never triggered even though we have it enabled.

Further investigation suggests that it requires some additional configuration to work with typescript. I'll look into that and try and get it merged into eslint-plugin-shopify:

@BPScott
Copy link
Member Author

BPScott commented Oct 29, 2018

Yay I've found out that eslint should have already been complaining about circular dependencies, but it wasn't correctly configured for TypeScript files I'll stick that into this PR and favour it over depcruiser to identify circular imports - no point in adding a new tool when an existing one can suffice.


I want to make sure we're going down the right path.

Forcing me to think this through and offer good reasoning is super-appreciated. Thank you for doing so :)

What is the reason we can't import from index but Web and other sewing-kit apps are recommended to do so?

I think this isn't a "us vs web" thing but a "accessing sibling components vs accessing components that exist anywhere else in the hierarchy" thing. It seems "us vs web" because the majority use-case in Polaris is a shared component importing one of its sibling shared components (as we have a single components folder) while this is just one of several ways that one component can import another within web.

The existing rule of "import from the component index" only applies if the file you're importing is NOT a sibling of the current file. In web most of the time the shared component you're importing lives in a different folder to the current file. I think at the moment your concern that this PR would violate the web-foundations standards is because you're trying to apply the rule in all cases rather than noting this exception to the rule.

Web has just added an eslint rule to state that you shouldn't import from your parent, and should instead import directly from your sibling.

As that is a new rule, there is bound to be some existing violations as it is not currently being enforced in Web but they should be refactored.


To highlight the varying ways that you can import in Web, here's a list of places a component can live in Web ordered from on a scale of most to least specific :

  1. app/sections/<SectionName>/<PageName>/<PageName>.tsx
  2. app/sections/<SectionName>/<PageName>/components for components that are only used within that page
  3. app/sections/<SectionName>/components for components that only used within that section
  4. app/components for components that are used in all sections.

There's a a couple of ways you can import here:

  • Importing a file from a more specific location (e.g. app/components/Baz/Baz.tsx contains import {Foo} from 'app/sections/<SectionName>/components'). This is a no-no as it suggests that the importee is living in the wrong place.
  • Importing a file from a less specific location (e.g. app/sections/SectionName/SectionName.tsx or app/sections/SectionName/components/Bar/Bar.tsx contains import {Foo} from 'app/components'). I expect that this is the most common case in Web. We should import from the component index. There is no danger of circular dependencies as no file in app/components should reference a file in app/sections as it violates encapsulation expectations.
  • Importing a file from the same location (e.g. app/sections/SectionName/components/Bar contains import Qux from '../Qux'). This happens sometimes in Web, and always in Polaris. The direct file should be used to avoid the potential for circular dependencies.

As you can see the way component imports work in polaris (i.e you're importing a sibling) only crops up some of the time in web.

@kvendrik
Copy link
Member

Yay I've found out that eslint should have already been complaining about circular dependencies, but it wasn't correctly configured for TypeScript files I'll stick that into this PR and favour it over depcruiser to identify circular imports - no point in adding a new tool when an existing one can suffice.

🎉

The existing rule of "import from the component index" only applies if the file you're importing is NOT a sibling of the current file. In web most of the time the shared component you're importing lives in a different folder to the current file. I think at the moment your concern that this PR would violate the web-foundations standards is because you're trying to apply the rule in all cases rather than noting this exception to the rule.

I'm not as concerned with that we would have to follow the conventions Web is setting. If we decide our ideal convention of importing is different from Web that's something I'm good with I just think its probably a good idea to establish a convention. My question more was "assuming that importing from the index file is the ideal way of working when importing from a different folder I still don't quite understand why we would have technical limitations that would prevent us from doing so but Web doesn't?"

Web has just added an eslint rule to state that you shouldn't import from your parent, and should instead import directly from your sibling.

Totally agree with the rule. I feel though as if the core of the argument here is that we might not completly agree on where we wanna go with this.

My main point is that I think we should have a convention around imports and I usually feel like the conventions Web is setting are a good place to start.

To lay out the different parts of how those work in my experience:

Components, utilities etc are always imported from an index file:

// global component
import {FeedbackModal} from 'components';

// local component
import {Action} from './components';

The added rule seems to make sure that a path always leads to an individual file or folder. This for example happens in tests for a component where you would import the component from its actual file and not the index.

// bad
import FeedbackModal from '..';

// good
import FeedbackModal from '../FeedbackModal';

In conclusion I think its important we first establish what our ideal way of importing is going to be and then what we need to do to get there. I have assumed so far that we wanna do it the same way Web does it and the only thing I see standing in the way of that right now is the technical limitations around the index imports and awareness but I feel like you might have some additional ideas regarding how this should all work and we should probably align on them.

Having that said having linting help us make sure we don't create circular dependencies sounds great 💯

@BPScott
Copy link
Member Author

BPScott commented Nov 10, 2018

Yo @kvendrik, this has now been rebased and is ready for review.

I have not yet enabled the import/no-cycle eslint rule as there are still some circular dependencies present, and they require a bit more unpicking than changing import paths. I figured keep this PR to be just numerous brainless import changes and we can tackle the fiddly ones afterwards.

Running yarn run depcruise-validate to see what issues remain:

 warn no-circular: src/components/AppProvider/Intl.ts → src/components/AppProvider/utils/index.tsx
  warn no-circular: src/components/AppProvider/index.ts → src/components/UnstyledLink/index.ts
  warn no-circular: src/components/AppProvider/utils/index.tsx → src/components/AppProvider/Intl.ts
  warn no-circular: src/components/UnstyledLink/UnstyledLink.tsx → src/components/AppProvider/index.ts
  warn no-circular: src/components/UnstyledLink/index.ts → src/components/UnstyledLink/UnstyledLink.tsx

Conveniently I think these might be solved by your AppProvider refactor.

@BPScott
Copy link
Member Author

BPScott commented Nov 10, 2018

Info from the commit message about what's going on here:

Importing from the component index leads to circular depdendencies that
cause issues when trying to optimize builds upstream. See this essay
https://github.com/Shopify/sewing-kit/blob/2bbf40fee4b6fc6e8a2cbbab1b743b47532b98eb/src/tools/webpack/tests/config/rules/javascript.test.ts#L215-L268

This commit adds tooling to help idenify cycles and stop them, so that
sewing-kit can remove its workarounds.

  • Depdendency Cruiser (yarn run depcruise-validate) shall help
    idenfity cycles.
  • shopify/no-ancestor-directory-import is an eslint rule that bars
    imports from parent indexes, which is the most common cause of cycles.

Fix current ancestort imports to import from direct files. This is not
enforced in tests because. test files will not be imported by anything
else so there is no danger of cylical imports.

@BPScott BPScott added the 🤖Skip Changelog Causes CI to ignore changelog update check. label Nov 10, 2018
Copy link
Member

@kvendrik kvendrik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

Importing from the component index leads to circular depdendencies that
cause issues when trying to optimize builds upstream. See this essay
https://github.com/Shopify/sewing-kit/blob/2bbf40fee4b6fc6e8a2cbbab1b743b47532b98eb/src/tools/webpack/tests/config/rules/javascript.test.ts#L215-L268

This commit adds tooling to help idenify cycles and stop them, so that
sewing-kit can remove its workarounds.

* Depdendency Cruiser (`yarn run depcruise-validate`) shall help
idenfity cycles.
* shopify/no-ancestor-directory-import is an eslint rule that bars
imports from parent indexes, which is the most common cause of cycles.

Fix current ancestort imports to import from direct files. This is not
enforced in tests because. test files will not be imported by anything
else so there is no danger of cylical imports.
@BPScott BPScott requested a deployment to polaris-react-pr-475 November 13, 2018 18:54 Abandoned
@BPScott BPScott merged commit 3ab258a into master Nov 13, 2018
@BPScott BPScott deleted the depcruiser branch November 13, 2018 18:59
@AndrewMusgrave AndrewMusgrave temporarily deployed to production November 14, 2018 20:23 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🤖Skip Changelog Causes CI to ignore changelog update check.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants